#include "mkvmuxfilter.hpp"
#include "cenumpins.hpp"
#include <new>
#include <cassert>
#include <vfwmsgs.h>
#include <uuids.h>
#include <evcode.h>
#ifdef _DEBUG
#include "odbgstream.hpp"
using std::endl;
#endif

using std::wstring;
//using std::wistringstream;

namespace MkvMux
{

// {E1642F6D-181E-41ab-9FFB-9321D3A5DD09}
extern const CLSID CLSID_MkvMux = 
{ 0xe1642f6d, 0x181e, 0x41ab, { 0x9f, 0xfb, 0x93, 0x21, 0xd3, 0xa5, 0xdd, 0x9 } };


Filter::Lock::Lock(Filter* p) :
    m_pFilter(p)
{
    const HRESULT hr = m_pFilter->EnterCriticalSection();
    hr;
    assert(SUCCEEDED(hr));
}


Filter::Lock::~Lock()
{
    m_pFilter->LeaveCriticalSection();
}


HRESULT Filter::EnterCriticalSection()
{
    const DWORD timeout = 5000;
    DWORD index;

    const HRESULT hr = CoWaitForMultipleHandles(
                            0,  //wait flags
                            timeout, 
                            1, 
                            &m_hMutex, 
                            &index);

    //despite the "S" in this name, this is an error                        
    if (hr == RPC_S_CALLPENDING)
        return VFW_E_TIMEOUT;

    if (FAILED(hr))
        return hr;
                
    assert(index == 0);
    
    return S_OK;
}



void Filter::LeaveCriticalSection()
{
    const BOOL b = ReleaseMutex(m_hMutex);
    b;
    assert(b);
}



HRESULT CreateInstance(
    IClassFactory* pClassFactory,
    IUnknown* pOuter, 
    const IID& iid, 
    void** ppv)
{
    if (ppv == 0)
        return E_POINTER;
        
    *ppv = 0;

    if ((pOuter != 0) && (iid != __uuidof(IUnknown)))
        return E_INVALIDARG;
    
    Filter* p = new (std::nothrow) Filter(pClassFactory, pOuter);
    
    if (p == 0)
        return E_OUTOFMEMORY;
        
    assert(p->m_nondelegating.m_cRef == 0);
    
    const HRESULT hr = p->m_nondelegating.QueryInterface(iid, ppv);
    
    if (SUCCEEDED(hr))
    {
        assert(*ppv);
        assert(p->m_nondelegating.m_cRef == 1);
        
        return S_OK;
    }
    
    assert(*ppv == 0);
    assert(p->m_nondelegating.m_cRef == 0);

    delete p;
    p = 0;

    return hr;
}


#pragma warning(disable:4355)  //'this' ptr in member init list
Filter::Filter(IClassFactory* pClassFactory, IUnknown* pOuter)
    : m_pClassFactory(pClassFactory),
      m_nondelegating(this),
      m_pOuter(pOuter ? pOuter : &m_nondelegating),
      m_state(State_Stopped),
      m_clock(0),
      m_inpin_video(this),
      m_inpin_audio(this),
      m_outpin(this)
{
    m_pClassFactory->LockServer(TRUE);
            
    m_hMutex = CreateMutex(0, 0, 0);
    assert(m_hMutex);  //TODO
    
    m_info.pGraph = 0;
    m_info.achName[0] = L'\0';
    
#ifdef _DEBUG        
    odbgstream os;
    os << "mkvmux::ctor" << endl;
#endif    
}
#pragma warning(default:4355)


Filter::~Filter()
{
#ifdef _DEBUG
    odbgstream os;
    os << "mkvmux::dtor" << endl;
#endif

    const BOOL b = CloseHandle(m_hMutex);
    assert(b);
    b;

    m_pClassFactory->LockServer(FALSE);
}      



Filter::nondelegating_t::nondelegating_t(Filter* p)
    : m_pFilter(p),
      m_cRef(0)  //see CreateInstance
{
}


Filter::nondelegating_t::~nondelegating_t()
{
}


HRESULT Filter::nondelegating_t::QueryInterface(
    const IID& iid, 
    void** ppv)
{
    if (ppv == 0)
        return E_POINTER;
        
    IUnknown*& pUnk = reinterpret_cast<IUnknown*&>(*ppv);
     
    if (iid == __uuidof(IUnknown))    
    {
        pUnk = this;  //must be nondelegating
    }   
    else if ((iid == __uuidof(IBaseFilter)) ||
             (iid == __uuidof(IMediaFilter)) ||
             (iid == __uuidof(IPersist)))
    {
        pUnk = static_cast<IBaseFilter*>(m_pFilter);
    }
    else if (iid == __uuidof(IMediaSeeking))
    {
        pUnk = static_cast<IMediaSeeking*>(m_pFilter);
    }
    else if (iid == __uuidof(IAMFilterMiscFlags))
    {
        pUnk = static_cast<IAMFilterMiscFlags*>(m_pFilter);
    }
    else
    {
#if 0
        wodbgstream os;
        os << "mp3source::filter::QI: iid=" << IIDStr(iid) << std::endl;
#endif        
        pUnk = 0;
        return E_NOINTERFACE;
    }

    pUnk->AddRef();
    return S_OK;
}


ULONG Filter::nondelegating_t::AddRef()
{
    return InterlockedIncrement(&m_cRef);
}

    
ULONG Filter::nondelegating_t::Release()
{
    if (LONG n = InterlockedDecrement(&m_cRef))
        return n;
    
    delete m_pFilter;
    return 0;
}


HRESULT Filter::QueryInterface(const IID& iid, void** ppv)
{
    return m_pOuter->QueryInterface(iid, ppv);
}


ULONG Filter::AddRef()
{
    return m_pOuter->AddRef();
}


ULONG Filter::Release()
{
    return m_pOuter->Release();
}


HRESULT Filter::GetClassID(CLSID* p)
{
    if (p == 0)
        return E_POINTER;
        
    *p = CLSID_MkvMux;
    return S_OK;
}



HRESULT Filter::Stop()
{
    //Stop is a synchronous operation: when it completes,
    //the filter is stopped.

    Lock lock(this);

#if 0
    odbgstream os;
    os << "mux::filter::stop (begin)" << endl;
#endif
        
    switch (m_state)
    {
        case State_Paused:
        case State_Running:
        
            m_outpin.Final();  //close mkv file if req'd
            m_inpin_audio.Final();
            m_inpin_video.Final();
            
            break;            

        case State_Stopped:
        default:
            break;
    }
    
    m_state = State_Stopped;

#if 0
    os << "mux::filter::stop (end)" << endl;
#endif

    return S_OK;
}


HRESULT Filter::Pause()
{
    //Unlike Stop(), Pause() can be asynchronous (that's why you have
    //GetState()).  We could use that here to build the samples index.    

    Lock lock(this);

#if 0    
    odbgstream os;
    os << "mux::filter::pause" << endl;
#endif

    switch (m_state)
    {
        case State_Stopped:
            m_inpin_video.Init();
            m_inpin_audio.Init();
            m_outpin.Init();
            break;

        case State_Running:
        case State_Paused:
        default:
            break;            
    }
    
    m_state = State_Paused;
    return S_OK;
}


HRESULT Filter::Run(REFERENCE_TIME start)
{
    Lock lock(this);
    
#if 0
    odbgstream os;
    os << "mux::filter::run" << endl;
#endif

    switch (m_state)
    {
        case State_Stopped:
            m_inpin_video.Init();
            m_inpin_audio.Init();
            m_outpin.Init();
            break;

        case State_Paused:        
        case State_Running:
        default:
            m_inpin_video.Run();
            m_inpin_audio.Run();
            break;            
    }

    m_start = start;
    m_state = State_Running;
    
    return S_OK;
}


HRESULT Filter::GetState( 
    DWORD /* timeout */ ,
    FILTER_STATE* p)
{
    if (p == 0)
        return E_POINTER;
        
    Lock lock(this);
    
    *p = m_state;
    
    return S_OK;
}



HRESULT Filter::SetSyncSource( 
    IReferenceClock* clock)
{
    Lock lock(this);
    
    if (m_clock)
        m_clock->Release();
        
    m_clock = clock;
    
    if (m_clock)
        m_clock->AddRef();

    return S_OK;
}


HRESULT Filter::GetSyncSource( 
    IReferenceClock** pclock)
{
    if (pclock == 0)
        return E_POINTER;
        
    Lock lock(this);
    
    IReferenceClock*& clock = *pclock;
        
    clock = m_clock;
    
    if (clock)
        clock->AddRef();

    return S_OK;
}


HRESULT Filter::EnumPins(IEnumPins** pp)
{
    Lock lock(this);
    
    IPin* const pa[3] = { &m_inpin_video, &m_inpin_audio, &m_outpin };
    
    return CEnumPins::CreateInstance(pa, 3, pp);
}



HRESULT Filter::FindPin( 
    LPCWSTR id,
    IPin** pp)
{
    if (pp == 0)
        return E_POINTER;
        
    IPin*& p = *pp;
    p = 0;
    
    if (id == 0)
        return E_INVALIDARG;
        
    enum { n = 3 };

    Pin* const pins[n] =
    {
        &m_inpin_video,
        &m_inpin_audio,
        &m_outpin
    };
    
    for (int i = 0; i < n; ++i)
    {
        Pin* const pin = pins[i];
        
        if (wcscmp(id, pin->m_id) == 0)
        {   
            p = pin;
            p->AddRef();
            
            return S_OK;
        }
    }

    return VFW_E_NOT_FOUND;
}



HRESULT Filter::QueryFilterInfo(FILTER_INFO* p)
{
    if (p == 0)
        return E_POINTER;
        
    Lock lock(this);
    
    enum { size = sizeof(p->achName)/sizeof(WCHAR) };
    const errno_t e = wcscpy_s(p->achName, size, m_info.achName);
    assert(e == 0);
    e;

    p->pGraph = m_info.pGraph;
    
    if (p->pGraph)
        p->pGraph->AddRef();
        
    return S_OK;
}


HRESULT Filter::JoinFilterGraph( 
    IFilterGraph *pGraph,
    LPCWSTR name)
{
    Lock lock(this);
    
    //NOTE: 
    //No, do not adjust reference counts here!
    //Read the docs for the reasons why.
    //ENDNOTE.    
    
    m_info.pGraph = pGraph;

    if (name == 0)
        m_info.achName[0] = L'\0';
    else
    {
        enum { size = sizeof(m_info.achName)/sizeof(WCHAR) };
        const errno_t e = wcscpy_s(m_info.achName, size, name);
        assert(e == 0);  //TODO
        e;
    }
    
    return S_OK;
}


HRESULT Filter::QueryVendorInfo(LPWSTR* pstr)
{
    if (pstr == 0)
        return E_POINTER;
        
    wchar_t*& str = *pstr;
    
    str = 0;
    return E_NOTIMPL;
}


HRESULT Filter::GetCapabilities(DWORD* pdw)
{
    if (pdw == 0)
        return E_POINTER;
        
    DWORD& dw = *pdw;
    
    //TODO: this could be conditional upon whether we're stopped or not,
    //or whether the output is connected or not.
    dw = AM_SEEKING_CanGetCurrentPos;

    return S_OK;
}


HRESULT Filter::CheckCapabilities(DWORD* pdw)
{
    if (pdw == 0)
        return E_POINTER;
        
    DWORD& dw = *pdw;

    const DWORD dwRequested = dw;
    
    if (dwRequested == 0)
        return E_INVALIDARG;
    
    DWORD dwActual;

    const HRESULT hr = GetCapabilities(&dwActual);
    assert(SUCCEEDED(hr)); hr;
    assert(dw);  //TODO: could be 0 if GetCap is conditional
    
    dw &= dwActual;
    
    if (dw == 0)
        return E_FAIL;
        
    return (dw == dwRequested) ? S_OK : S_FALSE;
}


HRESULT Filter::IsFormatSupported(const GUID* p)
{
    if (p == 0)
        return E_POINTER;
        
    const GUID& fmt = *p;
        
    if (fmt == TIME_FORMAT_MEDIA_TIME)
        return S_OK;

    //TODO
    //if (fmt != TIME_FORMAT_FRAME)
    //    return S_FALSE;
    
    return S_FALSE;
}


HRESULT Filter::QueryPreferredFormat(GUID* p)
{
    if (p == 0)
        return E_POINTER;
        
    *p = TIME_FORMAT_MEDIA_TIME;
    return S_OK;
}


HRESULT Filter::GetTimeFormat(GUID* p)
{
    if (p == 0)
        return E_POINTER;
        
    *p = TIME_FORMAT_MEDIA_TIME;
    return S_OK;
}


HRESULT Filter::IsUsingTimeFormat(const GUID* p)
{
    if (p == 0)
        return E_INVALIDARG;
        
    return (*p == TIME_FORMAT_MEDIA_TIME) ? S_OK : S_FALSE;
}


HRESULT Filter::SetTimeFormat(const GUID* p)
{
    if (p == 0)
        return E_INVALIDARG;
        
    const GUID& g = *p;
    
    if (g == TIME_FORMAT_MEDIA_TIME)
        return S_OK;
        
    return E_INVALIDARG;
}
    

HRESULT Filter::GetDuration(LONGLONG* p)
{
    Lock lock(this);
    
    return m_outpin.GetDuration(p);
}


HRESULT Filter::GetStopPosition(LONGLONG* p)
{
    Lock lock(this);
    
    return m_outpin.GetStopPosition(p);
}


HRESULT Filter::GetCurrentPosition(LONGLONG* p)
{
    Lock lock(this);
    
    return m_outpin.GetCurrentPosition(p);
}


HRESULT Filter::ConvertTimeFormat( 
    LONGLONG* ptgt,
    const GUID* ptgtfmt,
    LONGLONG src,
    const GUID* psrcfmt)
{
    if (ptgt == 0)
        return E_POINTER;
        
    LONGLONG& tgt = *ptgt;
        
    const GUID& tgtfmt = ptgtfmt ? *ptgtfmt : TIME_FORMAT_MEDIA_TIME;
    const GUID& srcfmt = psrcfmt ? *psrcfmt : TIME_FORMAT_MEDIA_TIME;
    
    if (tgtfmt != TIME_FORMAT_MEDIA_TIME)
        return E_INVALIDARG;
        
    if (srcfmt != TIME_FORMAT_MEDIA_TIME)
        return E_INVALIDARG;
        
    if (src < 0)
        return E_INVALIDARG;
            
    tgt = src;
    return S_OK;                
}


HRESULT Filter::SetPositions( 
    LONGLONG* pCurrPos,
    DWORD dwCurr,
    LONGLONG* pStopPos,
    DWORD dwStop)
{
    Lock lock(this);
    
    return m_outpin.SetPositions(pCurrPos, dwCurr, pStopPos, dwStop);
}


HRESULT Filter::GetPositions( 
    LONGLONG* pCurrPos,
    LONGLONG* pStopPos)
{
    Lock lock(this);
    
    return m_outpin.GetPositions(pCurrPos, pStopPos);
}


HRESULT Filter::GetAvailable( 
    LONGLONG* pEarliest,
    LONGLONG* pLatest)
{
    Lock lock(this);
    
    return m_outpin.GetAvailable(pEarliest, pLatest);
}


HRESULT Filter::SetRate(double r)
{
    if (r == 1)
        return S_OK;
        
    if (r <= 0)
        return E_INVALIDARG;
        
    return E_NOTIMPL;  //TODO: better return here?
}


HRESULT Filter::GetRate(double* p)
{
    if (p == 0)
        return E_POINTER;
        
    *p = 1;
    return S_OK;
}


HRESULT Filter::GetPreroll(LONGLONG* p)
{
    if (p == 0)
        return E_POINTER;
        
    *p = 0;  //?
    return S_OK;
}


ULONG Filter::GetMiscFlags()
{
    return AM_FILTER_MISC_FLAGS_IS_RENDERER;
}


HRESULT Filter::OnEndOfStream()
{
#if 1
    //NOTE: it appears that you must do both of send EOS downstream
    //to the file writer, and act as renderer (as described below):
    if (IPin* pin = m_outpin.m_connection)
    {
        const HRESULT hr = pin->EndOfStream();
        hr;
        assert(SUCCEEDED(hr));
    }
    
    //"End-of-Stream Notifications"
    //http://msdn.microsoft.com/en-us/library/dd375604(VS.85).aspx
    
    //The key to getting this to work is to treat this muxer as
    //as renderer.  We do that by supporting both IMediaSeeking and 
    //IAMFilterMiscFlags.  As the API states:
    //
    //(begin quote)
    //To determine the number of streams, the Filter Graph Manager 
    //counts the number of filters that support seeking (through 
    //IMediaSeeking or IMediaPosition) and have a rendered input pin, 
    //which is defined as an input pin with no corresponding outputs. 
    //The Filter Graph Manager determines whether a pin is rendered 
    //in one of two ways:
    //
    //(1)  The pin's IPin::QueryInternalConnections method returns zero 
    //     in the nPin parameter.
    //(2)  The filter exposes the IAMFilterMiscFlags interface and 
    //     returns the AM_FILTER_MISC_FLAGS_IS_RENDERER flag.
    //(end quote)
    //    
    //See also:
    //"IPin::QueryInternalConnections Method"
    //http://msdn.microsoft.com/en-us/library/dd390431(VS.85).aspx
    //
    //(begin quote)
    //This method has another use that is now deprecated: The Filter 
    //Graph Manager treats a filter as being a renderer filter if at 
    //least one input pin implements this method but returns zero in nPin. 
    //If you are writing a new renderer filter, however, you should 
    //implement the IAMFilterMiscFlags interface instead of using this 
    //method to indicate that the filter is a renderer.
    //(end quote)

#if 0    
    odbgstream os;
    os << "muxer::filter::OnEndOfStream: sending EC_COMPLETE" << endl;
#endif

    _COM_SMARTPTR_TYPEDEF(IMediaEventSink, __uuidof(IMediaEventSink));
    
    const IMediaEventSinkPtr pSink(m_info.pGraph);
    assert(bool(pSink));
    
    const HRESULT hr = pSink->Notify(EC_COMPLETE, S_OK, 0);
    hr;
    assert(SUCCEEDED(hr));
    
#else
    //TODO: I thought that sending EOS should be enough, but apparently
    //the EC_COMPLETE is not being delivered to the main loop.
    //I don't know why.  As a workaround, just manually send
    //a user event, so we know something gets delivered.
    
    _COM_SMARTPTR_TYPEDEF(IMediaEventSink, __uuidof(IMediaEventSink));
    
    const IMediaEventSinkPtr pSink(m_info.pGraph);
    assert(bool(pSink));

    const HRESULT hr = pSink->Notify(EC_USER + 0x100, 0, 0);
    hr;
#endif
    
    return S_OK;
}


} //end namespace MkvMux
